反射基础
当我们需要检查或修改 Java 虚拟机中正在运行的应用程序的运行时行为时通常会使用反射。反射提高了程序的扩展性,程序可以通过使用外部用户定义类的完全限定名(fully-qualified names)来创建扩展性对象的实例或者获取类的信息。
我们在业务代码中很少用到反射,但是在一些框架中如 Spring,MyBatis 等都大量的使用了反射。一个最常见的例子是,Spring 创建 Bean 用的就是反射机制。
类的反射
通过类的完全限定名(fully-qualified names),我们可以获取到这个类的一个类实例。
方法一:Class.forName()
这是最常用的一种方法。
Class<?> clazz = Class.forName("tech.devguide.reflection.classload.model.User");
方法二:ClassLoader.loadClass()
Class<?> clazz = ClassLoadTest.class.getClassLoader().loadClass("tech.devguide.reflection.classload.model.User");
Class.forName()
vs ClassLoader.loadClass()
两者都能获加载类,并且取到类的信息,但是两者是有一些区别的。
Class.forName()
- 使用类加载器加装 class,类记载器为调用者的类加载器,一般为应用类加载器:
AppClassLoader
。 - 初始化类,所有静态成员变量会被初始化,静态代码块会被执行。
- 使用类加载器加装 class,类记载器为调用者的类加载器,一般为应用类加载器:
ClassLoader.loadClass()
- 只加载类,并不初始化类。
实例化
方法一:使用Class#newInstance()
方法实例化
Class<?> clazz = Class.forName("tech.devguide.reflection.classload.model.User");
User user = (User) clazz.newInstance();
这个方法从JDK9开始已被标记为过时。以下为关于过时的说明
This method propagates any exception thrown by the nullary constructor, including a checked exception. Use of this method effectively bypasses the compile-time exception checking that would otherwise be performed by the compiler. The
Constructor.newInstance
method avoids this problem by wrapping any exception thrown by the constructor in a (checked)InvocationTargetException
.The call
clazz. newInstance()
can be replaced by
clazz. getDeclaredConstructor().newInstance()
方法二:使用构造函数实例化
先对 User
类做如下修改:
@Data
public class User {
private String name;
private int age;
public User() {
}
public User(String name) {
this.name = name;
}
private User(String name, int age) {
this.name = name;
this.age = age;
}
}
使用无参构造函数实例化
Class<?> clazz = Class.forName("tech.devguide.reflection.classload.model.User");
Constructor<User> constructor = (Constructor<User>) clazz.getConstructor();
user = constructor.newInstance();
使用有参构造函数实例化
Class<?> clazz = Class.forName("tech.devguide.reflection.classload.model.User");
Constructor<User> constructor = (Constructor<User>) clazz.getConstructor(String.class);
User user = constructor.newInstance("张三");
System.out.println(user.getName());
如果使用了lombok
在获取有参构造函数时可能会报错,错误信息如下:
Fatal error compiling: java.lang.NoSuchFieldError:
Class com.sun.tools.javac.tree.JCTree$JCImport does not have member field 'com.sun.tools.javac.tree.JCTree qualid'
这是因为 lombok
版本太低,与高版本 JDK 不兼容的问题导致,lombok 支持 JDK 21 的最低版本为 1.18.30
,具体可见:
使用私有有参构造函数实例化
Class<?> clazz = Class.forName("tech.devguide.reflection.classload.model.User");
Constructor<User> constructor = (Constructor<User>) clazz.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
User user = constructor.newInstance("张三", 12);
System.out.println(user.getName());
这里需要注意的是需要通过 constructor.setAccessible(true)
设置下访问权限,否则该构造函数是无法访问的。
在通过参数获取构造函数时,要注意基本类型和包装类型之间是不等价的,如上面的例子,如果将 int.class
改为 Integer.class
是无法获取到构造函数的。
Class<?> clazz = Class.forName("tech.devguide.reflection.classload.model.User");
Constructor<User> constructor = (Constructor<User>) clazz.getDeclaredConstructor(String.class, Integer.class);
错误信息如下
Exception in thread "main" java.lang.NoSuchMethodException: tech.devguide.reflection.classload.model.User.<init>(java.lang.String,java.lang.Integer)
at java.base/java.lang.Class.getConstructor0(Class.java:3689)
at java.base/java.lang.Class.getDeclaredConstructor(Class.java:2858)
at tech.devguide.reflection.classload.ClassLoadTest.main(ClassLoadTest.java:29)
Note
这里的 <init>
就是构造函数。具体在《JVM上篇:内存与垃圾回收篇--类加载子系统》章节由详细的讲解